跳到主要内容

CSS 中的 Grid 布局

grid 和 flex 区别是什么?适用什么场景?

  • Flexbox 是一维布局系统,适合做局部布局,比如导航栏组件。
  • Grid 是二维布局系统,通常用于整个页面的规划。

二者从应用场景来说并不冲突。虽然 Flexbox 也可以用于大的页面布局,但是没有 Grid 强大和灵活。二者结合使用更加轻松。

构建基础网格

创建一个新网页,给它外链一个新的样式表。将代码清单 6-1 复制到网页中。在下面的代码里,我给子元素加上了从 a 到 f 的字母,这样能清楚地看到网格里面每个元素的位置。

跟 Flexbox 类似,网格布局也是作用于两级的 DOM 结构。设置为 display: grid 的元素成为一个网格容器(grid container)。它的子元素则变成网格元素(grid items)

接下来,要用一些新的属性来定义网格的细节。

在支持网格布局的浏览器中,这段代码会渲染三列,共六个大小相等的盒子(如图 6-2 所示 )

首先,使用 display: grid 定义一个网格容器。容器会表现得像一个块级元素,100% 填充可用宽度。也

接下来是新属性:grid-template-columns 和 grid-template-rows。这两个属性定义了网格每行每列的大小。

本例使用了一种新单位 fr,代表每一列(或每一行)的分数单位(fraction unit)。这个单位跟 Flexbox 中 flex-grow 因子的表现一样。

grid-template-columns: 1fr 1fr 1fr 表示三列等宽。

不一定非得用分数单位,可以使用其他的单位,比如 px、em 或百分数。也可以混搭这几种单位,例如,grid-template-columns: 300px 1fr 定义了一个固定宽度为 300px 的列,后面跟着一个会填满剩余可用空间的列。2fr 的列宽是 1fr 的两倍。

最后,grid-gap 属性定义了每个网格单元之间的间距。也可以用两个值分别指定垂直和水平方向的间距(比如 grid-gap: 0.5em 1em)。

网格剖析

理解网格的各个部分很重要。前面已经提及网格容器和网格元素,这些是网格布局的基本元素。

  • 网格线(grid line)——网格线构成了网格的框架。一条网格线可以水平或垂直,并且位于一行或一列的任意一侧。如果指定了 grid-gap 的话,它就位于网格线上。
  • 网格轨道(grid track)—— 一个网格轨道是两条相邻网格线之间的空间。网格有水平轨道(行)和垂直轨道(列)。
  • 网格单元(grid cell)——网格上的单个空间,水平和垂直的网格轨道交叉重叠的部分。
  • 网格区域(grid area)——网格上的矩形区域,由一个到多个网格单元组成。该区域位于两条垂直网格线和两条水平网格线之间

构建网格布局时会涉及这些组成部分。比如声明 grid-template-columns: 1fr 1fr 1fr 就会定义三个等宽且垂直的网格轨道,同时还定义了四条垂直的网格线:一条在网格最左边,两条在每个网格轨道之间,还有一条在最右边。

使用 flex 那里的例子来说明,考虑如何用网格来实现

虚线标出了每个网格单元的位置。注意,某些部分跨越了好几个网格单元,也就是填充了更大的网格区域。

这个网格有两列和四行。前两个水平网格轨道分别是网页标题(Ink)和主导航菜单。主区域填满了第一个垂直轨道剩下的两个网格单元,两个侧边栏的板块分别放在第二个垂直轨道剩下的两个网格单元里。

提示

设计不必填满每个网格单元。在想留白的地方让网格单元为空即可。

编写网页的例子

可以在作者的 GitHub 上找到 源码

<!doctype html>
<head>
<style>
:root {
box-sizing: border-box;
}

*,
::before,
::after {
box-sizing: inherit;
}

body {
background-color: #709b90;
font-family: Helvetica, Arial, sans-serif;
}

.container {
display: grid;
/* 定义两个垂直的网格轨道 */
grid-template-columns: 2fr 1fr;
/* 定义四个水平轨道,大小为 auto */
grid-template-rows: repeat(4, auto);
grid-gap: 1.5em;
max-width: 1080px;
margin: 0 auto;
}

header,
nav {
/* 从1号垂直网格线跨越到 3 号垂直网格线 */
grid-column: 1 / 3;
/* 刚好占据一条水平网格轨道 */
grid-row: span 1;
}

.main {
/* 将其他网格元素定位到不同的网格线之间 */
grid-column: 1 / 2;
grid-row: 3 / 5;
}

.sidebar-top {
/* 将其他网格元素定位到不同的网格线之间 */
grid-column: 2 / 3;
grid-row: 3 / 4;
}

.sidebar-bottom {
/* 将其他网格元素定位到不同的网格线之间 */
grid-column: 2 / 3;
grid-row: 4 / 5;
}

.tile {
padding: 1.5em;
background-color: #fff;
}

.tile > :first-child {
margin-top: 0;
}

.tile * + * {
margin-top: 1.5em;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="page-heading">Ink</h1>
</header>
<nav>
<ul class="site-nav">
<li><a href="/">Home</a></li>
<li><a href="/features">Features</a></li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/support">Support</a></li>
<li class="nav-right">
<a href="/about">About</a>
</li>
</ul>
</nav>
<main class="main tile">
<h1>Team collaboration done right</h1>
<p>Thousands of teams from all over the
world turn to <b>Ink</b> to communicate
and get things done.</p>
</main>
<div class="sidebar-top tile">
<form class="login-form">
<h3>Login</h3>
<p>
<label for="username">Username</label>
<input id="username" type="text"
name="username"/>
</p>
<p>
<label for="password">Password</label>
<input id="password" type="password"
name="password"/>
</p>
<button type="submit">Login</button>
</form>
</div>
<div class="sidebar-bottom tile centered">
<small>Starting at</small>
<div class="cost">
<span class="cost-currency">$</span>
<span class="cost-dollars">20</span>
<span class="cost-cents">.00</span>
</div>
<a class="cta-button" href="/pricing">
Sign up
</a>
</div>
</div>
</body>

按照上面切割的网格,我们可以得到下面的网格布局

代码首先设置了网格容器,并用 grid-template-columnsgrid-template-rows 定义了网格轨道。因为列的分数单位分别是 2fr 和 1fr,所以第一列的宽度是第二列的两倍。定义行的时候用到了一个新方法:repeat() 函数。它在声明多个网格轨道的时候提供了简写方式。

grid-template-rows: repeat(4, auto);

定义了四个水平网格轨道,高度为 auto,这等价于 grid-template-rows: auto auto auto auto。轨道大小设置为 auto,轨道会根据自身内容扩展。

repeat() 符号还可以定义不同的重复模式,比如 repeat(3, 2fr 1fr) 会重复三遍这个模式,从而定义六个网格轨道,重复的结果是 2fr 1fr 2fr 1fr 2fr 1fr

还可以将 repeat() 作为一个更长的模式的一部分。比如 grid-template-columns: 1fr repeat(3, 3fr) 1fr 定义了一个 1fr 的列,接着是三个 3fr 的列,最后还有一个 1fr 的列(可以用 1fr 3fr 3fr 3fr 1fr 表示)。

可以看出来因为展开的写法无法一目了然,所以才产生了 repeat() 这种简写方式。

网格线的编号

网格轨道定义好之后,要将每个网格元素放到特定的位置上。浏览器给网格里的每个网格线都赋予了编号,如图 6-7 所示。CSS 用这些编号指出每个元素应该摆放的位置。

可以在 grid-column 和 grid-row 属性中用网格线的编号指定网格元素的位置。如果想要一个网格元素在垂直方向上跨越 1 号网格线到 3 号网格线,就需要给元素设置 grid-column: 1 / 3

或者设置 grid-row: 3 / 5 让元素在水平方向上跨越 3 号网格线到 5 号网格线。这两个属性一起就能指定一个元素应该放置的网格区域。

在本章的网页里,这些网格元素是按以下代码片段定位的

这段代码将 main 元素放在第一列(1 号到 2 号网格线之间),跨越第三行到第四行(3 号到 5 号网格线)的位置。侧边栏的两个板块放在右列(2 号到 3 号网格线之间),并且在第三行和第四行上下排列。

提示

这些属性实际上是简写属性:grid-column 是 grid-column-start 和 grid-column-end 的简写;grid-row 是 grid-row-start 和 grid-row-end 的简写。中间的斜线只在简写属性里用于区分两个值,斜线前后的空格不作要求。

定位 header 和 nav 的规则集稍有变化。以下代码片段用相同的规则集同时布局这两者。

header,
nav {
grid-column: 1 / 3;
grid-row: span 1;
}

代码里使用之前介绍的 grid-column 写法,让网格元素占满网格的宽度。其实还可以用一个特别的关键字 span 来指定 grid-row 和 grid-column 的值(这里用在了 grid-row 上)。

这个关键字告诉浏览器元素需要占据一个网格轨道。因为这里没有指出具体是哪一行,所以会根据网格元素的布局算法(placement algorithm)自动将其放到合适的位置。

References